package com.yzd.learnjava.concurrency;

import java.io.IOException;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import com.yzd.learnjava.util.Print;

public class BankTellerSimulation {
	static final int MAX_LINE_SIZE = 50;
	static final int ADJUSTMENT_PRIOD = 1000;
	public static void main(String[] args) throws IOException {
		ExecutorService exec = Executors.newCachedThreadPool();
		CustomerLine customers = new CustomerLine(MAX_LINE_SIZE);
		exec.execute(new CustomerGenerator(customers));
		exec.execute(new TellerManager(exec, customers, ADJUSTMENT_PRIOD));
		
		Print.print("Press 'Enter' to quit");
		System.in.read();
		
		exec.shutdownNow();
	}
}

class Customer {
	private final int serviceTime ;
	public Customer(int tm) {
		serviceTime = tm ;
	}
	public int getServiceTime() {
		return serviceTime;
	}
	public String toString() {
		return "[" + serviceTime + "]";
	}
}

class CustomerLine extends ArrayBlockingQueue<Customer>{
	public CustomerLine(int maxLineSize) {
		super(maxLineSize);
	}
	public String toString() {
		if(this.size() == 0) {
			return "[Empty]";
		}
		StringBuilder sb = new StringBuilder();
		for(Customer c : this) {
			sb.append(c);
		}
		return sb.toString();
	}
}

class CustomerGenerator implements Runnable{
	private CustomerLine customers ;
	private static Random random = new Random(47);
	public CustomerGenerator(CustomerLine cq) {
		this.customers = cq ;
	}
	
	public void run() {
		try {
			while(!Thread.interrupted()) {
				TimeUnit.MILLISECONDS.sleep(random.nextInt(300));
				customers.put(new Customer(random.nextInt(1000)));
			}
		} catch (InterruptedException e) {
			Print.print("CustomerGenerator interrupted");
			e.printStackTrace();
		}
	}
}

class Teller implements Runnable, Comparable<Teller>{
	private static int counter = 0;
	private final int id = counter++;
	private int customerServed = 0;
	private CustomerLine customers;
	private boolean servingCustomerLine = true;
	public Teller(CustomerLine cq) {
		this.customers = cq;
	}
	public void run() {
		try {
			while(!Thread.interrupted()) {
				Customer customer = customers.take();
				TimeUnit.MILLISECONDS.sleep(customer.getServiceTime());
				synchronized (this) {
					customerServed++;
					while(!servingCustomerLine) {
						wait();
					}
				}
			}
		} catch (InterruptedException e) {
			Print.print(this + " interrupted");
			e.printStackTrace();
		}
		Print.print(this + " terminating");
	}
	
	public synchronized void doSomethingElse() {
		customerServed = 0;
		servingCustomerLine = false;
	}
	
	public synchronized void serveCustomeLine() {
		assert !servingCustomerLine : "already serving: " + this;
		servingCustomerLine = true;
		notifyAll();
	}
	public String toString() {
		return "Teller " + id + " ";
	}
	public String shortString() {
		return "T" + id;
	}
	
	public synchronized int compareTo(Teller other) {
		return customerServed < other.customerServed ? -1 : (customerServed == other.customerServed ? 0 : 1);
	}
}

class TellerManager implements Runnable{
	private ExecutorService exec ;
	private CustomerLine customers ;
	private PriorityQueue<Teller> workingTellers = new PriorityQueue<Teller>();
	private Queue<Teller> tellerDoingOtherThings = new LinkedList<Teller>();
	private int adjustmentPeriod;
	private static Random rand = new Random(47);
	
	public TellerManager(ExecutorService e , CustomerLine customers , int adjustmentPeriod) {
		this.exec = e;
		this.customers = customers;
		this.adjustmentPeriod = adjustmentPeriod;
		
		Teller teller = new Teller(customers);
		exec.execute(teller);
		workingTellers.add(teller);
	}
	
	public void adjustTellerNumber() {
		if(customers.size() / workingTellers.size() > 2) {
			if(tellerDoingOtherThings.size() > 0) {
				Teller teller = tellerDoingOtherThings.remove();
				teller.serveCustomeLine();
				workingTellers.offer(teller);
				return;
			}
			Teller teller = new Teller(customers);
			exec.execute(teller);
			workingTellers.add(teller);
			return;
		}
		
		if(workingTellers.size() > 1 && customers.size() / workingTellers.size() < 2) {
			reassignOneTeller();
		}
		
		if(customers.size() == 0) {
			while(workingTellers.size() > 1) {
				reassignOneTeller();
			}
		}
	}
	
	private void reassignOneTeller() {
		Teller teller = workingTellers.poll();
		teller.doSomethingElse();
		tellerDoingOtherThings.offer(teller);
	}
	
	public void run() {
		try {
			while(!Thread.interrupted()) {
				TimeUnit.MILLISECONDS.sleep(adjustmentPeriod);
				adjustTellerNumber();
				Print.printnb(customers + " {");
				for(Teller t : workingTellers) {
					Print.printnb(t.shortString() + " ");
				}
				Print.print("}");
			}
		} catch (InterruptedException e) {
			Print.print(this + " interrupted");
			e.printStackTrace();
		}
		Print.print(this + " terminating");
	}
	
	public String toString() {
		return "TellerManager";
	}
}























